<script>
var dtmf = {
	play: false,
	ready: false,
	keys: {},
	audio: null,
	dst: null,
	rec: null,
	data: [],
	tone0: null,
	tone1: null,
	sequence: null,
	time_on: -1,
	time_off: -1,
	space: -1,
	duration: -1,
	button_replay: null,
	button_restop: null,
	restop_flag: false,
	init: function () {
		dtmf.audio = new window.AudioContext();
		dtmf.dst = dtmf.audio.createMediaStreamDestination();
		dtmf.rec = new MediaRecorder(dtmf.dst.stream);
		dtmf.rec.ondataavailable = function(evt){dtmf.data.push(evt.data)};
		dtmf.rec.onstop = function(evt) {document.getElementById('dtmf_record').title ="num_"+document.getElementById('dtmf_sequence').value+".ogg"; document.getElementById('dtmf_record').src = URL.createObjectURL(new Blob(dtmf.data, { 'type' : 'audio/ogg; codecs=opus' }))};
		dtmf.ready = true;
	},
	freqs: function (key) {
		let freqs = dtmf.keys[key];
		if (freqs !== undefined) {
			return freqs;
		}
		return null;
	},
	start: function (event = null, replay = null) {
		if (!dtmf.ready) {
			dtmf.init();
		}
		if (dtmf.play) {
			return false;
		}
		dtmf.duration = parseInt(document.getElementById('dtmf_duration').value);
		dtmf.space = parseInt(document.getElementById('dtmf_space').value);
		let key = -1;
		if (replay !== null) {
			key = replay;
		} else if (event instanceof MouseEvent) {
			key = this.innerText.trim();
			dtmf.sequence.value += "" + key;
		} else {
			key = event.key;
		}
		key = key.toUpperCase();
		let tones = dtmf.freqs(key);
		if (tones === null) {
			return false;
		}
		dtmf.play = true;
		dtmf.tone0 = dtmf.audio.createOscillator();
		dtmf.tone0.type = 'sine';
		dtmf.tone0.frequency.setValueAtTime(tones[0], dtmf.audio.currentTime);
		dtmf.tone1 = dtmf.audio.createOscillator();
		dtmf.tone1.type = 'sine';
		dtmf.tone1.frequency.setValueAtTime(tones[1], dtmf.audio.currentTime);
		if (replay !== null) {
			dtmf.tone1.connect(dtmf.dst);
			dtmf.tone0.connect(dtmf.dst);
		} else {
			dtmf.tone0.connect(dtmf.audio.destination);
			dtmf.tone1.connect(dtmf.audio.destination);
		}
		dtmf.tone0.start();
		dtmf.tone1.start();
		dtmf.time_on = new Date().getTime();
		return true;
	},
	stop: function () {
		dtmf.time_off = new Date().getTime();
		let ms = dtmf.time_off - dtmf.time_on;
		if (ms < dtmf.duration) {
			setTimeout(dtmf.stop, dtmf.duration + 1 - ms);
			return;
		}
		if (dtmf.tone0 !== null) {
			dtmf.tone0.stop();
			dtmf.tone0 = null;
		}
		if (dtmf.tone1 !== null) {
			dtmf.tone1.stop();
			dtmf.tone1 = null;
		}
		dtmf.play = false;
	},
	restop: function () {
		dtmf.restop_flag = true;
	},
	backspace: function () {
		dtmf.sequence.value = dtmf.sequence.value.substring(0, dtmf.sequence.value.length - 1);
	},
	clear: function () {
		document.getElementById('dtmf_sequence').value = '';
	},
	replay: function (index =  - 1, space = false) {
		if (index !== -1) {
			dtmf.stop();
		} else {
			dtmf.button_replay.disabled = true;
			dtmf.keyboard(false);
			dtmf.button_restop.disabled = false;
			dtmf.data = [];
			dtmf.rec.start();
		}
		if (space) {
			setTimeout(dtmf.replay, dtmf.space, index, false);
			return;
		}
		if (!dtmf.restop_flag && index < dtmf.sequence.value.length) {
			dtmf.sequence.selectionStart = index + 1;
			dtmf.sequence.selectionEnd = index + 2;
			dtmf.sequence.focus();
			index++;
			key = dtmf.sequence.value.charAt(index);
			dtmf.start(null, key);
			setTimeout(dtmf.replay, dtmf.duration, index, true);
		} else {
			dtmf.button_replay.disabled = false;
			dtmf.keyboard(true);
			dtmf.button_restop.disabled = true;
			dtmf.restop_flag = false;
			dtmf.sequence.focus();
			if (dtmf.rec !== null && dtmf.rec.state == 'recording') {
				dtmf.rec.stop();
			}
		}
	},
	keyboard: function (enable) {
		buttons = document.querySelectorAll('#dtmf_keyboard button');
		buttons.forEach(function (button, index) {
			button.disabled = !enable;
		});
	},
	load: function () {
		dtmf.sequence = document.getElementById('dtmf_sequence');
		dtmf.button_replay = document.getElementById('dtmf_replay');
		dtmf.button_restop = document.getElementById('dtmf_restop');
		let f = [1209, 1336, 1477, 1633, 697, 770, 852, 941];
		dtmf.keys = {
			'1':[f[0], f[4]],
			'2':[f[1], f[4]],
			'3':[f[2], f[4]],
			'A':[f[3], f[4]],
			'4':[f[0], f[5]],
			'5':[f[1], f[5]],
			'6':[f[2], f[5]],
			'B':[f[3], f[5]],
			'7':[f[0], f[6]],
			'8':[f[1], f[6]],
			'9':[f[2], f[6]],
			'C':[f[3], f[6]],
			'*':[f[0], f[7]],
			'0':[f[1], f[7]],
			'#':[f[2], f[7]],
			'D':[f[3], f[7]]
		};
		buttons = document.querySelectorAll('#dtmf_keyboard button');
		buttons.forEach(function (button, index) {
			button.addEventListener('mousedown', dtmf.start);
			button.addEventListener('mouseup', dtmf.stop);
			button.addEventListener('mouseleave', dtmf.stop);
		});
		dtmf.sequence.focus();
	}
};
document.addEventListener('DOMContentLoaded', dtmf.load);
</script>

<style>
:root{
	--c-back-body:#f9f9f9;
	--c-back-main:#fff;
	--c-back-button:#efefef;
	--c-back-button-hover:#e7e7e7;
	--c-back-plate:#f7f7f7;
	--c-back-progress:#f1f1f1;
	--c-border:#aaa;
	--c-border-banner:#bbb;
	--c-text:#111;
	}
	@media (prefers-color-scheme:dark){
	:root{
	--c-back-body:#323436;
	--c-back-main:#2c2c2c;
	--c-back-button:#464a4b;
	--c-back-button-hover:#535353;
	--c-back-plate:#3d4042;
	--c-border:#787878;
	--c-border-banner:#5d6265;
	--c-text:#d7d7d7;
	}}
	body,html{margin:0;padding:0}
	html{height:100vh;width:100%}
	body{background-color:var(--c-back-body);font-family:'Open Sans',sans-serif;font-size:16px;color:var(--c-text);min-height:100vh;min-width:280px;display:flex;flex-direction:column}
	input,button,textarea,select,.button{outline:0;font-family:FontAwesome,'Open Sans',sans-serif;font-size:14px}
	input,button,.button{padding:5px;margin:3px;border:1px solid var(--c-border);background-color:var(--c-back-main);color:var(--c-text)}
	.button{text-align:center;text-decoration:none}
	input:disabled,button:disabled,.button:disabled{color:var(--c-border-banner)}
	input[type=button],input[type=submit],button,.button{padding:5px 10px;background-color:var(--c-back-button);cursor:pointer}
	input[type=button]:hover:enabled,input[type=submit]:hover:enabled,button:hover:enabled,.button:hover{background-color:var(--c-back-button-hover)}
	input:read-only{background-color:var(--c-back-plate)}
	textarea{box-sizing:border-box;border:1px solid var(--c-border);background-color:var(--c-back-main);color:var(--c-text)}
	select{padding:2px;background-color:var(--c-back-main);color:var(--c-text)}
	select,select:disabled{border:1px solid var(--c-border)}
	#content{line-height:1.75em}
	#content .plate{border:1px solid var(--c-border-banner);background-color:var(--c-back-plate);padding:15px;margin:15px 0}
	#content .form .line{display:flex;flex-wrap:wrap;margin-bottom:5px}
	#content .form .line:last-of-type{margin-bottom:0}
	#content .form .line>:not(.item):first-child{margin-left:0}
	#content .form .item{display:flex;flex-direction:column}
	#content .form .item:not(:last-child){margin-right:5px}
	#content .form .field{display:flex;flex-direction:row;align-items:center}
	#content .form .field>input:first-child{margin-left:0}
	#dtmf_keyboard button {
		width: 3em;
		height: 3em;
	}

	#dtmf_keyboard span{
		width: 1.5em;
		display: inline-block;
	}

	#dtmf_panel {
		display: flex;
		flex-direction: column;
		flex-grow: 1;
	}

	#dtmf_panel textarea {
		width: 100%;
	}

</style>
<div id="content">
<h1>DTMF Generator</h1>
<div class="brief">
	<div>
		Phone dial tones online synthesizer. Dial keyboard emulator.
	</div>
</div>
<div style="display: flex;flex-direction: row;flex-wrap: wrap;">
	<div id="dtmf_keyboard" class="plate" style="margin: 0.5em 0;">
		<div>
			<button>1</button>
			<button>2</button>
			<button>3</button>
			<span></span>
			<button>A</button>
		</div>
		<div>
			<button>4</button>
			<button>5</button>
			<button>6</button>
			<span></span>
			<button>B</button>
		</div>
		<div>
			<button>7</button>
			<button>8</button>
			<button>9</button>
			<span></span>
			<button>C</button>
		</div>
		<div>
			<button>*</button>
			<button>0</button>
			<button>#</button>
			<span></span>
			<button>D</button>
		</div>
	</div>
	<div style="padding: 0 0.5em;"></div>
	<div id="dtmf_panel" class="plate form" style="margin: 0.5em 0;">
		<div class="line">
			<div class="item" style="flex-grow: 1;">
				<div class="label">
					Sequence
				</div>
				<div class="field">
					<textarea spellcheck="false" id="dtmf_sequence" onkeydown="return dtmf.start(event);" onkeyup="dtmf.stop();"></textarea>
				</div>
			</div>
		</div>
		<div class="line">
			<button id="dtmf_replay" onclick="dtmf.replay();" style="flex-grow: 1;">
				Replay
			</button>
			<button id="dtmf_restop" onclick="dtmf.restop();" disabled="">
				Stop
			</button>			
			<button onclick="dtmf.backspace();">
				Back
			</button>
			<button style="margin-right: 0;" onclick="dtmf.clear();">
				Clear
			</button>
			
		</div>
		<div class="line">
			<div class="item">
				<div class="label">
					Duration [ms]
				</div>
				<div class="field">
					<input type="number" id="dtmf_duration" min="35" max="1000" value="150">
				</div>
			</div>
			<div class="item">
				<div class="label">
					Space [ms]
				</div>
				<div class="field">
					<input type="number" id="dtmf_space" min="5" max="1000" value="50">
				</div>
			</div>
		</div>
		<div><audio id="dtmf_record" controls></audio></div>
	</div>
</div>
</div>
